import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi

private class AdMessagesHistoryContextImpl {
    final class CachedMessage: Equatable, Codable {
        enum CodingKeys: String, CodingKey {
            case opaqueId
            case messageType
            case title
            case text
            case textEntities
            case media
            case contentMedia
            case color
            case backgroundEmojiId
            case url
            case buttonText
            case sponsorInfo
            case additionalInfo
            case canReport
            case minDisplayDuration
            case maxDisplayDuration
        }
        
        enum MessageType: Int32, Codable {
            case sponsored = 0
            case recommended = 1
        }
        
        public let opaqueId: Data
        public let messageType: MessageType
        public let title: String
        public let text: String
        public let textEntities: [MessageTextEntity]
        public let media: [Media]
        public let contentMedia: [Media]
        public let color: PeerNameColor?
        public let backgroundEmojiId: Int64?
        public let url: String
        public let buttonText: String
        public let sponsorInfo: String?
        public let additionalInfo: String?
        public let canReport: Bool
        public let minDisplayDuration: Int32?
        public let maxDisplayDuration: Int32?
        
        public init(
            opaqueId: Data,
            messageType: MessageType,
            title: String,
            text: String,
            textEntities: [MessageTextEntity],
            media: [Media],
            contentMedia: [Media],
            color: PeerNameColor?,
            backgroundEmojiId: Int64?,
            url: String,
            buttonText: String,
            sponsorInfo: String?,
            additionalInfo: String?,
            canReport: Bool,
            minDisplayDuration: Int32?,
            maxDisplayDuration: Int32?
        ) {
            self.opaqueId = opaqueId
            self.messageType = messageType
            self.title = title
            self.text = text
            self.textEntities = textEntities
            self.media = media
            self.contentMedia = contentMedia
            self.color = color
            self.backgroundEmojiId = backgroundEmojiId
            self.url = url
            self.buttonText = buttonText
            self.sponsorInfo = sponsorInfo
            self.additionalInfo = additionalInfo
            self.canReport = canReport
            self.minDisplayDuration = minDisplayDuration
            self.maxDisplayDuration = maxDisplayDuration
        }

        public init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)

            self.opaqueId = try container.decode(Data.self, forKey: .opaqueId)
            
            if let messageType = try container.decodeIfPresent(Int32.self, forKey: .messageType) {
                self.messageType = MessageType(rawValue: messageType) ?? .sponsored
            } else {
                self.messageType = .sponsored
            }
            
            self.title = try container.decode(String.self, forKey: .title)
            self.text = try container.decode(String.self, forKey: .text)
            self.textEntities = try container.decode([MessageTextEntity].self, forKey: .textEntities)

            let mediaData = try container.decode([Data].self, forKey: .media)
            self.media = mediaData.compactMap { data -> Media? in
                return PostboxDecoder(buffer: MemoryBuffer(data: data)).decodeRootObject() as? Media
            }
            
            let contentMediaData = try container.decode([Data].self, forKey: .contentMedia)
            self.contentMedia = contentMediaData.compactMap { data -> Media? in
                return PostboxDecoder(buffer: MemoryBuffer(data: data)).decodeRootObject() as? Media
            }
            
            self.color = try container.decodeIfPresent(Int32.self, forKey: .color).flatMap { PeerNameColor(rawValue: $0) }
            self.backgroundEmojiId = try container.decodeIfPresent(Int64.self, forKey: .backgroundEmojiId)

            self.url = try container.decode(String.self, forKey: .url)
            self.buttonText = try container.decode(String.self, forKey: .buttonText)
            
            self.sponsorInfo = try container.decodeIfPresent(String.self, forKey: .sponsorInfo)
            self.additionalInfo = try container.decodeIfPresent(String.self, forKey: .additionalInfo)
            
            self.canReport = try container.decodeIfPresent(Bool.self, forKey: .canReport) ?? false
            
            self.minDisplayDuration = try container.decodeIfPresent(Int32.self, forKey: .minDisplayDuration)
            self.maxDisplayDuration = try container.decodeIfPresent(Int32.self, forKey: .maxDisplayDuration)
        }

        public func encode(to encoder: Encoder) throws {
            var container = encoder.container(keyedBy: CodingKeys.self)

            try container.encode(self.opaqueId, forKey: .opaqueId)
            try container.encode(self.messageType.rawValue, forKey: .messageType)
            try container.encode(self.title, forKey: .title)
            try container.encode(self.text, forKey: .text)
            try container.encode(self.textEntities, forKey: .textEntities)

            let mediaData = self.media.map { media -> Data in
                let encoder = PostboxEncoder()
                encoder.encodeRootObject(media)
                return encoder.makeData()
            }
            try container.encode(mediaData, forKey: .media)
            
            let contentMediaData = self.contentMedia.map { media -> Data in
                let encoder = PostboxEncoder()
                encoder.encodeRootObject(media)
                return encoder.makeData()
            }
            try container.encode(contentMediaData, forKey: .contentMedia)

            try container.encodeIfPresent(self.color?.rawValue, forKey: .color)
            try container.encodeIfPresent(self.backgroundEmojiId, forKey: .backgroundEmojiId)
            
            try container.encode(self.url, forKey: .url)
            try container.encode(self.buttonText, forKey: .buttonText)
            
            try container.encodeIfPresent(self.sponsorInfo, forKey: .sponsorInfo)
            try container.encodeIfPresent(self.additionalInfo, forKey: .additionalInfo)
            
            try container.encode(self.canReport, forKey: .canReport)
            
            try container.encodeIfPresent(self.minDisplayDuration, forKey: .minDisplayDuration)
            try container.encodeIfPresent(self.maxDisplayDuration, forKey: .maxDisplayDuration)
        }

        public static func ==(lhs: CachedMessage, rhs: CachedMessage) -> Bool {
            if lhs.opaqueId != rhs.opaqueId {
                return false
            }
            if lhs.messageType != rhs.messageType {
                return false
            }
            if lhs.title != rhs.title {
                return false
            }
            if lhs.text != rhs.text {
                return false
            }
            if lhs.textEntities != rhs.textEntities {
                return false
            }
            if lhs.media.count != rhs.media.count {
                return false
            }
            for i in 0 ..< lhs.media.count {
                if !lhs.media[i].isEqual(to: rhs.media[i]) {
                    return false
                }
            }
            if lhs.contentMedia.count != rhs.contentMedia.count {
                return false
            }
            for i in 0 ..< lhs.contentMedia.count {
                if !lhs.contentMedia[i].isEqual(to: rhs.contentMedia[i]) {
                    return false
                }
            }
            if lhs.url != rhs.url {
                return false
            }
            if lhs.buttonText != rhs.buttonText {
                return false
            }
            if lhs.sponsorInfo != rhs.sponsorInfo {
                return false
            }
            if lhs.additionalInfo != rhs.additionalInfo {
                return false
            }
            if lhs.canReport != rhs.canReport {
                return false
            }
            if lhs.minDisplayDuration != rhs.minDisplayDuration {
                return false
            }
            if lhs.maxDisplayDuration != rhs.maxDisplayDuration {
                return false
            }
            return true
        }

        func toMessage(peerId: PeerId, transaction: Transaction) -> Message? {
            var attributes: [MessageAttribute] = []

            let mappedMessageType: AdMessageAttribute.MessageType
            switch self.messageType {
            case .sponsored:
                mappedMessageType = .sponsored
            case .recommended:
                mappedMessageType = .recommended
            }
            let adAttribute = AdMessageAttribute(
                opaqueId: self.opaqueId,
                messageType: mappedMessageType,
                url: self.url,
                buttonText: self.buttonText,
                sponsorInfo: self.sponsorInfo,
                additionalInfo: self.additionalInfo,
                canReport: self.canReport,
                hasContentMedia: !self.contentMedia.isEmpty,
                minDisplayDuration: self.minDisplayDuration,
                maxDisplayDuration: self.maxDisplayDuration
            )
            attributes.append(adAttribute)
            if !self.textEntities.isEmpty {
                let entitiesAttribute = TextEntitiesMessageAttribute(entities: self.textEntities)
                attributes.append(entitiesAttribute)
            }

            var messagePeers = SimpleDictionary<PeerId, Peer>()

            if let peer = transaction.getPeer(peerId) {
                messagePeers[peer.id] = peer
            }
            
            let author: Peer = TelegramChannel(
                id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(1)),
                accessHash: nil,
                title: self.title,
                username: nil,
                photo: [],
                creationDate: 0,
                version: 0,
                participationStatus: .left,
                info: .broadcast(TelegramChannelBroadcastInfo(flags: [])),
                flags: [],
                restrictionInfo: nil,
                adminRights: nil,
                bannedRights: nil,
                defaultBannedRights: nil,
                usernames: [],
                storiesHidden: nil,
                nameColor: self.color ?? .blue,
                backgroundEmojiId: self.backgroundEmojiId,
                profileColor: nil,
                profileBackgroundEmojiId: nil,
                emojiStatus: nil,
                approximateBoostLevel: nil,
                subscriptionUntilDate: nil,
                verificationIconFileId: nil,
                sendPaidMessageStars: nil,
                linkedMonoforumId: nil
            )
            messagePeers[author.id] = author
            
            let messageHash = (self.text.hashValue &+ 31 &* peerId.hashValue) &* 31 &+ author.id.hashValue
            let messageStableVersion = UInt32(bitPattern: Int32(truncatingIfNeeded: messageHash))
            
            return Message(
                stableId: 0,
                stableVersion: messageStableVersion,
                id: MessageId(peerId: peerId, namespace: Namespaces.Message.Local, id: 0),
                globallyUniqueId: nil,
                groupingKey: nil,
                groupInfo: nil,
                threadId: nil,
                timestamp: Int32.max - 1,
                flags: [.Incoming],
                tags: [],
                globalTags: [],
                localTags: [],
                customTags: [],
                forwardInfo: nil,
                author: author,
                text: self.text,
                attributes: attributes,
                media: !self.contentMedia.isEmpty ? self.contentMedia : self.media,
                peers: messagePeers,
                associatedMessages: SimpleDictionary<MessageId, Message>(),
                associatedMessageIds: [],
                associatedMedia: [:],
                associatedThreadInfo: nil,
                associatedStories: [:]
            )
        }
    }

    private let queue: Queue
    private let account: Account
    private let peerId: EnginePeer.Id
    private let messageId: EngineMessage.Id?

    private let maskAsSeenDisposables = DisposableDict<Data>()

    struct CachedState: Codable, PostboxCoding {
        enum CodingKeys: String, CodingKey {
            case timestamp
            case interPostInterval
            case messages
        }

        var timestamp: Int32
        var interPostInterval: Int32?
        var messages: [CachedMessage]

        init(timestamp: Int32, interPostInterval: Int32?, messages: [CachedMessage]) {
            self.timestamp = timestamp
            self.interPostInterval = interPostInterval
            self.messages = messages
        }

        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)

            self.timestamp = try container.decode(Int32.self, forKey: .timestamp)
            self.interPostInterval = try container.decodeIfPresent(Int32.self, forKey: .interPostInterval)
            self.messages = try container.decode([CachedMessage].self, forKey: .messages)
        }

        func encode(to encoder: Encoder) throws {
            var container = encoder.container(keyedBy: CodingKeys.self)

            try container.encode(self.timestamp, forKey: .timestamp)
            try container.encodeIfPresent(self.interPostInterval, forKey: .interPostInterval)
            try container.encode(self.messages, forKey: .messages)
        }

        init(decoder: PostboxDecoder) {
            self.timestamp = decoder.decodeInt32ForKey("timestamp", orElse: 0)
            self.interPostInterval = decoder.decodeOptionalInt32ForKey("interPostInterval")
            if let messagesData = decoder.decodeOptionalDataArrayForKey("messages") {
                self.messages = messagesData.compactMap { data -> CachedMessage? in
                    return try? AdaptedPostboxDecoder().decode(CachedMessage.self, from: data)
                }
            } else {
                self.messages = []
            }
        }

        func encode(_ encoder: PostboxEncoder) {
            encoder.encodeInt32(self.timestamp, forKey: "timestamp")
            if let interPostInterval = self.interPostInterval {
                encoder.encodeInt32(interPostInterval, forKey: "interPostInterval")
            } else {
                encoder.encodeNil(forKey: "interPostInterval")
            }
            encoder.encodeDataArray(self.messages.compactMap { message -> Data? in
                return try? AdaptedPostboxEncoder().encode(message)
            }, forKey: "messages")
        }

        public static func getCached(postbox: Postbox, peerId: PeerId) -> Signal<CachedState?, NoError> {
            return postbox.transaction { transaction -> CachedState? in
                let key = ValueBoxKey(length: 8)
                key.setInt64(0, value: peerId.toInt64())
                if let entry = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedAdMessageStates, key: key))?.get(CachedState.self) {
                    return entry
                } else {
                    return nil
                }
            }
        }

        public static func setCached(transaction: Transaction, peerId: PeerId, state: CachedState?) {
            let key = ValueBoxKey(length: 8)
            key.setInt64(0, value: peerId.toInt64())
            let id = ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedAdMessageStates, key: key)
            if let state = state, let entry = CodableEntry(state) {
                transaction.putItemCacheEntry(id: id, entry: entry)
            } else {
                transaction.removeItemCacheEntry(id: id)
            }
        }
    }
    
    struct State: Equatable {
        var interPostInterval: Int32?
        var startDelay: Int32?
        var betweenDelay: Int32?
        var messages: [Message]

        static func ==(lhs: State, rhs: State) -> Bool {
            if lhs.interPostInterval != rhs.interPostInterval {
                return false
            }
            if lhs.startDelay != rhs.startDelay {
                return false
            }
            if lhs.betweenDelay != rhs.betweenDelay {
                return false
            }
            if lhs.messages.count != rhs.messages.count {
                return false
            }
            for i in 0 ..< lhs.messages.count {
                if lhs.messages[i].id != rhs.messages[i].id {
                    return false
                }
                if lhs.messages[i].stableId != rhs.messages[i].stableId {
                    return false
                }
            }
            return true
        }
    }
    
    let state = Promise<State>()
    private var stateValue: State? {
        didSet {
            if let stateValue = self.stateValue, stateValue != oldValue {
                self.state.set(.single(stateValue))
            }
        }
    }

    private var isActivated: Bool = false
    private let disposable = MetaDisposable()
    
    init(queue: Queue, account: Account, peerId: EnginePeer.Id, messageId: EngineMessage.Id?, activateManually: Bool) {
        self.queue = queue
        self.account = account
        self.peerId = peerId
        self.messageId = messageId

        self.stateValue = State(interPostInterval: nil, messages: [])

        if messageId == nil {
            self.state.set(CachedState.getCached(postbox: account.postbox, peerId: peerId)
            |> mapToSignal { cachedState -> Signal<State, NoError> in
                if let cachedState = cachedState, cachedState.timestamp >= Int32(Date().timeIntervalSince1970) - 5 * 60 {
                    return account.postbox.transaction { transaction -> State in
                        return State(interPostInterval: cachedState.interPostInterval, messages: cachedState.messages.compactMap { message -> Message? in
                            return message.toMessage(peerId: peerId, transaction: transaction)
                        })
                    }
                } else {
                    return .single(State(interPostInterval: nil, messages: []))
                }
            })
        }

        if !activateManually {
            self.activate()
        }
    }
    
    deinit {
        self.disposable.dispose()
        self.maskAsSeenDisposables.dispose()
    }
    
    func activate() {
        if self.isActivated {
            return
        }
        self.isActivated = true
        
        let peerId = self.peerId
        let accountPeerId = self.account.peerId
        let account = self.account
        let messageId = self.messageId
        
        let signal: Signal<(interPostInterval: Int32?, startDelay: Int32?, betweenDelay: Int32?, messages: [Message]), NoError> = account.postbox.transaction { transaction -> Api.InputPeer? in
            return transaction.getPeer(peerId).flatMap(apiInputPeer)
        }
        |> mapToSignal { inputPeer -> Signal<(interPostInterval: Int32?, startDelay: Int32?, betweenDelay: Int32?, messages: [Message]), NoError> in
            guard let inputPeer else {
                return .single((nil, nil, nil, []))
            }
            var flags: Int32 = 0
            if let _ = messageId {
                flags |= (1 << 0)
            }
            return account.network.request(Api.functions.messages.getSponsoredMessages(flags: flags, peer: inputPeer, msgId: messageId?.id))
            |> map(Optional.init)
            |> `catch` { _ -> Signal<Api.messages.SponsoredMessages?, NoError> in
                return .single(nil)
            }
            |> mapToSignal { result -> Signal<(interPostInterval: Int32?, startDelay: Int32?, betweenDelay: Int32?, messages: [Message]), NoError> in
                guard let result = result else {
                    return .single((nil, nil, nil, []))
                }

                return account.postbox.transaction { transaction -> (interPostInterval: Int32?, startDelay: Int32?, betweenDelay: Int32?, messages: [Message]) in
                    switch result {
                    case let .sponsoredMessages(_, postsBetween, startDelay, betweenDelay, messages, chats, users):
                        let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
                        updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)

                        var parsedMessages: [CachedMessage] = []

                        for message in messages {
                            switch message {
                            case let .sponsoredMessage(flags, randomId, url, title, message, entities, photo, media, color, buttonText, sponsorInfo, additionalInfo, minDisplayDuration, maxDisplayDuration):
                                var parsedEntities: [MessageTextEntity] = []
                                if let entities = entities {
                                    parsedEntities = messageTextEntitiesFromApiEntities(entities)
                                }
                                
                                let isRecommended = (flags & (1 << 5)) != 0
                                let canReport = (flags & (1 << 12)) != 0
                                
                                var nameColorIndex: Int32?
                                var backgroundEmojiId: Int64?
                                if let color {
                                    switch color {
                                    case let .peerColor(_, color, backgroundEmojiIdValue):
                                        nameColorIndex = color
                                        backgroundEmojiId = backgroundEmojiIdValue
                                    default:
                                        break
                                    }
                                }
                                
                                let photo = photo.flatMap { telegramMediaImageFromApiPhoto($0) }
                                let contentMedia = textMediaAndExpirationTimerFromApiMedia(media, peerId).media
                                
                                parsedMessages.append(CachedMessage(
                                    opaqueId: randomId.makeData(),
                                    messageType: isRecommended ? .recommended : .sponsored,
                                    title: title,
                                    text: message,
                                    textEntities: parsedEntities,
                                    media: photo.flatMap { [$0] } ?? [],
                                    contentMedia: contentMedia.flatMap { [$0] } ?? [],
                                    color: nameColorIndex.flatMap { PeerNameColor(rawValue: $0) },
                                    backgroundEmojiId: backgroundEmojiId,
                                    url: url,
                                    buttonText: buttonText,
                                    sponsorInfo: sponsorInfo,
                                    additionalInfo: additionalInfo,
                                    canReport: canReport,
                                    minDisplayDuration: minDisplayDuration,
                                    maxDisplayDuration: maxDisplayDuration
                                ))
                            }
                        }

                        if messageId == nil {
                            CachedState.setCached(transaction: transaction, peerId: peerId, state: CachedState(timestamp: Int32(Date().timeIntervalSince1970), interPostInterval: postsBetween, messages: parsedMessages))
                        }
                        
                        return (postsBetween, startDelay, betweenDelay, parsedMessages.compactMap { message -> Message? in
                            return message.toMessage(peerId: peerId, transaction: transaction)
                        })
                    case .sponsoredMessagesEmpty:
                        return (nil, nil, nil, [])
                    }
                }
            }
        }
        
        self.disposable.set((signal
        |> deliverOn(self.queue)).start(next: { [weak self] interPostInterval, startDelay, betweenDelay, messages in
            guard let strongSelf = self else {
                return
            }
            strongSelf.stateValue = State(interPostInterval: interPostInterval, startDelay: startDelay, betweenDelay: betweenDelay, messages: messages)
        }))
    }

    func markAsSeen(opaqueId: Data) {
        let signal: Signal<Never, NoError> = self.account.network.request(Api.functions.messages.viewSponsoredMessage(randomId: Buffer(data: opaqueId)))
        |> `catch` { _ -> Signal<Api.Bool, NoError> in
            return .single(.boolFalse)
        }
        |> ignoreValues
        self.maskAsSeenDisposables.set(signal.start(), forKey: opaqueId)
    }
    
    func markAction(opaqueId: Data, media: Bool, fullscreen: Bool) {
        _internal_markAdAction(account: self.account, opaqueId: opaqueId, media: media, fullscreen: fullscreen)
    }
    
    func remove(opaqueId: Data) {
        if var stateValue = self.stateValue {
            if let index = stateValue.messages.firstIndex(where: { $0.adAttribute?.opaqueId == opaqueId }) {
                stateValue.messages.remove(at: index)
                self.stateValue = stateValue
            }
        }
        
        let peerId = self.peerId
        let _ = (self.account.postbox.transaction { transaction -> Void in
            let key = ValueBoxKey(length: 8)
            key.setInt64(0, value: peerId.toInt64())
            let id = ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedAdMessageStates, key: key)
            guard var cachedState = transaction.retrieveItemCacheEntry(id: id)?.get(CachedState.self) else {
                return
            }
            if let index = cachedState.messages.firstIndex(where: { $0.opaqueId == opaqueId }) {
                cachedState.messages.remove(at: index)
                if let entry = CodableEntry(cachedState) {
                    transaction.putItemCacheEntry(id: id, entry: entry)
                }
            }
        }).start()
    }
}

public class AdMessagesHistoryContext {
    private let queue = Queue()
    private let impl: QueueLocalObject<AdMessagesHistoryContextImpl>
    public let peerId: EnginePeer.Id
    public let messageId: EngineMessage.Id?
    
    public var state: Signal<(interPostInterval: Int32?, messages: [Message], startDelay: Int32?, betweenDelay: Int32?), NoError> {
        return Signal { subscriber in
            let disposable = MetaDisposable()
            
            self.impl.with { impl in
                let stateDisposable = impl.state.get().start(next: { state in
                    subscriber.putNext((state.interPostInterval, state.messages, state.startDelay, state.betweenDelay))
                })
                disposable.set(stateDisposable)
            }
            
            return disposable
        }
    }
    
    public init(account: Account, peerId: EnginePeer.Id, messageId: EngineMessage.Id? = nil, activateManually: Bool = false) {
        self.peerId = peerId
        self.messageId = messageId
        
        let queue = self.queue
        self.impl = QueueLocalObject(queue: queue, generate: {
            return AdMessagesHistoryContextImpl(queue: queue, account: account, peerId: peerId, messageId: messageId, activateManually: activateManually)
        })
    }

    public func markAsSeen(opaqueId: Data) {
        self.impl.with { impl in
            impl.markAsSeen(opaqueId: opaqueId)
        }
    }
    
    public func markAction(opaqueId: Data, media: Bool, fullscreen: Bool) {
        self.impl.with { impl in
            impl.markAction(opaqueId: opaqueId, media: media, fullscreen: fullscreen)
        }
    }
    
    public func remove(opaqueId: Data) {
        self.impl.with { impl in
            impl.remove(opaqueId: opaqueId)
        }
    }
    
    public func activate() {
        self.impl.with { impl in
            impl.activate()
        }
    }
}


func _internal_markAdAction(account: Account, opaqueId: Data, media: Bool, fullscreen: Bool) {
    var flags: Int32 = 0
    if media {
        flags |= (1 << 0)
    }
    if fullscreen {
        flags |= (1 << 1)
    }
    let signal = account.network.request(Api.functions.messages.clickSponsoredMessage(flags: flags, randomId: Buffer(data: opaqueId)))
    |> `catch` { _ -> Signal<Api.Bool, NoError> in
        return .single(.boolFalse)
    }
    |> ignoreValues
    let _ = signal.start()
}

func _internal_markAdAsSeen(account: Account, opaqueId: Data) {
    let signal = account.network.request(Api.functions.messages.viewSponsoredMessage(randomId: Buffer(data: opaqueId)))
    |> `catch` { _ -> Signal<Api.Bool, NoError> in
        return .single(.boolFalse)
    }
    |> ignoreValues
    let _ = signal.start()
}
